//#include <sys/param.h>  // endian-ness?
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <cairo.h>
#include <cairo-xlib.h>

#include "mcp.h"
#include "ui_cairo.h"
#include "uic_xlib.h"

namespace uic
{

// Hopefully-common starting values until these are obtained the Right Way.
/*
int GenericEvent::mask.shift = ShiftMask,
    GenericEvent::mask.lock = LockMask,
    GenericEvent::mask.control = ControlMask,
    GenericEvent::mask.alt = Mod1Mask,
    GenericEvent::mask.num_lock = Mod2Mask,
    GenericEvent::mask.scroll_lock = Mod3Mask,
    GenericEvent::mask.meta = Mod4Mask,
    GenericEvent::mask.compose = Mod5Mask;
*/
//  { int shift, lock, control, alt, num_lock, scroll_lock, meta, compose }
// typedef struct { int shift, lock, control,
//                  alt, num_lock, scroll_lock, meta, compose; } mod_values;
GenericEvent::mod_values GenericEvent::mask =
    { ShiftMask, LockMask, ControlMask,
      Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, Mod5Mask };

UIC_Xlib::UIC_Xlib ()
    : display(0),
#ifdef PIXMAP_BUFFER
      pixmap(0),
#endif
      surface(0)
    {}

UIC_Xlib::~UIC_Xlib ()
    {
    if ( surface != 0 ) cairo_surface_destroy(surface);
#ifdef PIXMAP_BUFFER
    if ( pixmap != 0 )
        { XFreePixmap(display, pixmap); XFreeGC(display, window_gc); }
#endif
    if ( display != 0 ) XCloseDisplay(display);
    }

//    if ( surface != 0 )
//        cairo_surface_destroy(surface);

cairo_t *UIC_Xlib::get_cairo (CairoUI *ui)
    {
    assert(surface != 0);
    return cairo_create(surface);
    }

#ifdef PIXMAP_BUFFER
void UIC_Xlib::resized (CairoUI *ui)
    {
    if ( pixmap != 0 )
        { mcp->log_verbose(1, "Freeing pixmap (%d) and gc (%d)\n", pixmap, window_gc);
        XFreePixmap(display, pixmap); XFreeGC(display, window_gc); }

    // TODO  handle BadAlloc
    pixmap = XCreatePixmap(display, window, ui->width, ui->height,
                           DefaultDepth(display, screen));
    XGCValues values;
    values.function = GXcopy;
    values.graphics_exposures = 0;
    // TODO  handle the various Bad's possible here.
    window_gc = XCreateGC(display, pixmap, GCFunction | GCGraphicsExposures, &values);
mcp->log_verbose(1, "Created pixmap (%d) and gc (%d)\n", pixmap, window_gc);

    if ( surface != 0 )
        cairo_xlib_surface_set_drawable(surface, pixmap, ui->width, ui->height);
    else
        {
        surface = cairo_xlib_surface_create(display, pixmap,
                                            DefaultVisual(display, screen),
                                            ui->width, ui->height);
        if ( cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS )
            {
            mcp->log_verbose(1, "Creating cairo xlib surface: %s\n",
                             cairo_status_to_string(cairo_surface_status(surface)));
            // TODO  abort, retry, or ignore?
            }
        }

    }
#else
void UIC_Xlib::resized (CairoUI *ui)
    {
    if ( surface == 0 )
        {
        surface = cairo_xlib_surface_create(display, window,
                                            DefaultVisual(display, screen),
                                            ui->width, ui->height);
        if ( cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS )
            {
            mcp->log_verbose(1, "Creating cairo xlib surface: %s\n",
                             cairo_status_to_string(cairo_surface_status(surface)));
            // TODO  abort, retry, or ignore?
            }
        }
    cairo_xlib_surface_set_size(surface, ui->width, ui->height);
    }
#endif

void UIC_Xlib::perform_flush (CairoUI *ui)
    {
    cairo_surface_flush(surface);
#ifdef PIXMAP_BUFFER
    XCopyArea(display, pixmap, window, window_gc, 0, 0, ui->width, ui->height, 0, 0);
#endif
    XFlush(display);
    }

void UIC_Xlib::set_title (const char *str)
    {
    XTextProperty xtext_name;
    if ( !str ) str = "";
    XStringListToTextProperty((char **)&str, 1, &xtext_name);
    XSetWMName(display, window, &xtext_name);
    XFree(xtext_name.value);
    }

int UIC_Xlib::parse_color (const char *str, Color *color)
    {
    XColor xc;
    if ( XParseColor(display, ScreenOfDisplay(display, screen)->cmap,
         str, &xc) )
        {
        color->red   = (double)xc.red  /65535.0;
        color->green = (double)xc.green/65535.0;
        color->blue  = (double)xc.blue /65535.0;
        color->alpha = 1.0;
        return 1;
        }
    return 0;
    }

bool UIC_Xlib::active ()
    { return ( display != 0 ); }

// TODO  trap X errors for window creation n' such
bool UIC_Xlib::activate (CairoUI *ui)
    {
    int try_x, try_y;
    int width, height;
    XSizeHints size;

    if ( !(display = XOpenDisplay(0)) )
        {
        fprintf(stderr, "rmc-xmms2: XOpenDisplay: %s\n", strerror(errno));
        return false;
        }

    if ( ui->fd ) delete ui->fd;
    ui->fd = new int[1];
    ui->fd[0] = ConnectionNumber(display);
    ui->fd_count = 1;

    // TODO setup XAddConnectionWatch etc. here
    //      ?  is it still needed
    //      -  I can't test it if it is..

    screen = DefaultScreen(display);

    size.min_width  = 64;
    size.min_height = 48;
    size.base_width  = 160;
    size.base_height = 100;
    size.width  = size.base_width;
    size.height  = size.base_height;
    size.max_aspect.x = size.base_width;
    size.max_aspect.y = size.base_height;
    size.win_gravity = None;
    size.flags = PBaseSize | PMinSize;
              // PMaxSize, USPosition, USSize obsolete?

    ui->apply_prefs();

    try_x = 0;
    try_y = 0;
    width = size.base_width;
    height = size.base_height;
    {
    const char *p;
    if ( (p = ui->prefs->get("geometry")) != 0 )
        {
        if ( sscanf(p, "%dx%d+%d+%d", &width, &height, &try_x, &try_y) == 4 ||
             sscanf(p, "%dx%d", &width, &height) == 2 )
            mcp->log_verbose(10, "config geometry: %dx%d+%d+%d\n",
                             width, height, try_x, try_y);
        }
    if ( (p = ui->prefs->get("size")) != 0 )  // eh.. why not
        {
        if ( sscanf(p, "%dx%d", &width, &height) == 2 )
            mcp->log_verbose(10, "config size: %dx%d\n", width, height);
        }
    }

    window = XCreateSimpleWindow(display, RootWindow(display, screen),
                                 try_x, try_y, width, height, 0,
                                 WhitePixel(display, screen),
                                 BlackPixel(display, screen));
    XSetWMNormalHints(display, window, &size);

    XClassHint classhint;
    XTextProperty xtext_name;
    classhint.res_name = (char *)ui->default_window_title;
    classhint.res_class = (char *)"Rmc";
    XSetClassHint(display, window, &classhint);
    if ( XStringListToTextProperty(&classhint.res_name, 1, &xtext_name) != 0 )
        {
        XSetWMName(display, window, &xtext_name);
        XFree(xtext_name.value);
        }
#ifdef DEBUG
    else
        mcp->log_verbose(1, "error allocating window name!?");
#endif

    set_wm_icon(ui);

    XSelectInput(display, window,
                 ExposureMask | KeyPressMask | KeyReleaseMask |
                 ButtonPressMask | ButtonReleaseMask | 
                 EnterWindowMask | LeaveWindowMask | PointerMotionMask |
                 ExposureMask | VisibilityChangeMask | StructureNotifyMask);
                 // Button1MotionMask

    XMapWindow(display, window);
    XSync(display, false);

    // TODO  configure properly as the window is mapped/unmapped
    ui->width = 0;  // first_configure kludge
    ui->height = 0;
    return true;
    }

/*
void CairoUI::process_output (int fd)
    {
    flush();
    }
*/

void UIC_Xlib::fd_exception (CairoUI *ui, int fd_arg)
    {
    if ( surface ) cairo_surface_destroy(surface);
    surface = 0;
    if ( display ) XCloseDisplay(display);
    display = 0;
    if ( ui->fd ) delete ui->fd;
    ui->fd_count = 0;
    }

void UIC_Xlib::process_input (CairoUI *ui, int fd_arg)
    {
//    if ( fd != ConnectionNumber(display) )
//        XProcessInternalConnection(display, fd);
//    else
    while (XPending(display) > 0 )
        {
        XEvent ev;
        XNextEvent(display, &ev);

        switch ( ev.type )
            {
            case Expose:
                if ( ui->width == 0 || ui->height == 0 ) // Can occur when the
                    break;                               // window is first moved
                while ( XCheckTypedWindowEvent(display, window, Expose, &ev) )
                    ; // Skip ahead to the most recent expose
                ui->redraw(true);
                break;
            case EnterNotify:       ui->pointer_enter();
                break;
            case LeaveNotify:       ui->pointer_leave();
                break;
            case KeyPress:          key_press(ui, (XKeyEvent *)&ev);
                break;

            // Convert the following into the local GenericEvent struct
            case ConfigureNotify:
            case ButtonPress:
            case ButtonRelease:
            case MotionNotify:
                {
                GenericEvent gev;
                switch ( ev.type )
                    {
                    case ConfigureNotify:
                        // TODO better to just return if one is upcoming?
                        // skip ahead to the most recent resize
                        while ( XCheckTypedWindowEvent(display, window,
                                    ConfigureNotify, &ev) )
                            ;
                        gev.x = ((XConfigureEvent *)&ev)->x;
                        gev.y = ((XConfigureEvent *)&ev)->y;
                        gev.width = ((XConfigureEvent *)&ev)->width;
                        gev.height = ((XConfigureEvent *)&ev)->height;
                        if ( ui->width == 0 && ui->height == 0 )
                            ui->first_configure(&gev);
                        else
                            ui->configure(&gev);
                        break;

                    case ButtonPress:
/*
if ( ev.state & LockMask )
  mcp->log_verbose(11, "volume wheel modifier: LockMask \n");
if ( ev.state & Mod1Mask )
  mcp->log_verbose(11, "volume wheel modifier: Mod1Mask \n");
if ( ev.state & Mod2Mask )
  mcp->log_verbose(11, "volume wheel modifier: Mod2Mask \n");
if ( ev.state & Mod3Mask )
  mcp->log_verbose(11, "volume wheel modifier: Mod3Mask \n");
if ( ev.state & Mod4Mask )
  mcp->log_verbose(11, "volume wheel modifier: Mod4Mask \n");
if ( ev.state & Mod5Mask )
  mcp->log_verbose(11, "volume wheel modifier: Mod5Mask \n");
*/
                    case ButtonRelease:
                        // TODO deal with other mouse button configurations
                        //      (side buttons vs. wheel for some actions)
                        gev.x = ((XButtonEvent *)&ev)->x;
                        gev.y = ((XButtonEvent *)&ev)->y;
                        gev.button = ((XButtonEvent *)&ev)->button;
                        gev.modifier = ((XButtonEvent *)&ev)->state;
                        if ( ev.type == ButtonPress )
                            ui->button_press(&gev);
                        else
                            ui->button_release(&gev);
                        break;

                    case MotionNotify:
                        gev.x = ((XMotionEvent *)&ev)->x;
                        gev.y = ((XMotionEvent *)&ev)->y;
                        gev.modifier = ((XMotionEvent *)&ev)->state;
                        ui->motion(&gev);
                        break;
                    }
                }
            }

        ui->flush();
        }
    }

void UIC_Xlib::move (int new_x, int new_y)
    {
    // TODO does this return value mean anything?
    // FIXME handle failure to move the window?
    // FIXME possibly use a ConfigureRequest instead
    XMoveWindow(display, window, new_x, new_y);
    }

// TODO  configurable keys
// FIXME  convert to a GenericEvent and pass to one common key handler
void UIC_Xlib::key_press (CairoUI *ui, XKeyEvent *ev)
    {
//    KeyCode keycode = ev->keycode;
    switch ( XKeycodeToKeysym(display, ev->keycode, 0) )
        {
        case XK_X: case XK_x:
        case XK_Return: case XK_KP_Enter:
        case XK_Execute:
            mcp->command(COMMAND_PLAY);
            break;
        case XK_C: case XK_c:
        case XK_space: case XK_Pause: case XK_KP_5:
            mcp->command(COMMAND_PAUSE);
            break;
        case XK_V: case XK_v: case XK_KP_Delete:
            mcp->command(COMMAND_STOP);
            break;
        case XK_B: case XK_b: case XK_Right:
        case XK_KP_Right: case XK_KP_Add:
            mcp->command(COMMAND_NEXT);
            break;
        case XK_Z: case XK_z:
        case XK_BackSpace: case XK_Left:
        case XK_KP_Left: case XK_KP_Subtract:
            mcp->command(COMMAND_PREVIOUS);
            break;
        case XK_Home: case XK_KP_Home: case XK_Begin:
            mcp->command(COMMAND_FIRST);
            break;
        case XK_End: case XK_KP_End: 
            mcp->command(COMMAND_LAST);
            break;
        case XK_Up: case XK_KP_Up:
            mcp->change_volume(5, false);
            break;
        case XK_Down: case XK_KP_Down:
            mcp->change_volume(-5, false);
            break;
        }
    }

// I am uncertain whether this is a bug, but on my setup the icon in WM_HINTS
// and _NET_WM_ICON have to be the same size or it ignores the hint.
#define ICON_SIZE 64

// In theory this isn't Xlib-specific, but there's not much common code here.
void UIC_Xlib::set_wm_icon (CairoUI *ui)
    {
    cairo_t  *cr;
    cairo_surface_t *surface;

    // WMHints icon
    XWMHints *wm_hint;
    if ( !(wm_hint = XAllocWMHints()) )
        ; // TODO error handling mechanism

    wm_hint->icon_pixmap = XCreatePixmap(display, window, ICON_SIZE, ICON_SIZE,
                                         DefaultDepth(display, screen));
    wm_hint->icon_mask = XCreatePixmap(display, window, ICON_SIZE, ICON_SIZE,
                                       DefaultDepth(display, screen));
//    surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, 64, 64);
    surface = cairo_xlib_surface_create(display, wm_hint->icon_pixmap,
                                        DefaultVisual(display, screen),
                                        ICON_SIZE, ICON_SIZE);
    cr = cairo_create(surface);
    cairo_surface_destroy(surface);
    cairo_scale(cr, ICON_SIZE/100.0, ICON_SIZE/100.0);
    ui->draw_icon(cr, false);
    cairo_xlib_surface_set_drawable(surface, wm_hint->icon_mask,
                                    ICON_SIZE, ICON_SIZE);
    ui->draw_icon(cr, true);
    cairo_destroy(cr);

//    char *ptr = cairo_image_surface_get_data(surface);
//    icon_pixmap = XCreateBitmapFromData(display, window, ptr, 64, 64);

    wm_hint->flags = IconPixmapHint | IconMaskHint;
    XSetWMHints(display, window, wm_hint);
    XFree(wm_hint);

    // if ( _NET_WM_ICON is needed? )
        {
        int stride;
        long *icondata;
        unsigned char *data;

        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
                                             ICON_SIZE, ICON_SIZE);
        cr = cairo_create(surface);
        cairo_scale(cr, ICON_SIZE/100.0, ICON_SIZE/100.0);
        ui->draw_icon(cr, false);
        cairo_surface_flush(surface);

        stride = cairo_image_surface_get_stride(surface);
        data = cairo_image_surface_get_data(surface);
/*
FILE *fl;
fl = fopen("test", "w");
int i = 0;
for (int j = 0; j < ICON_SIZE; j++) {
 fprintf(fl, "%3d:", j);
 for (int k = 0; k < ICON_SIZE*4; k+=4) {
  i += 4;
  fprintf(fl, "  %d,%d,%d,%d", data[i], data[i+1], data[i+2], data[i+3]);
 }
 fprintf(fl, "\n");
}
fclose(fl);
*/
//        icondata = (long *)malloc((8 * 4) * (ICON_SIZE * ICON_SIZE + 2));
        icondata = new long[2 + ICON_SIZE * ICON_SIZE];
        // if ( !icondata )
            // .. and do what exactly?
        icondata[0] = ICON_SIZE;  /// First 2 values are
        icondata[1] = ICON_SIZE;  //  the pixmap's dimensions
        for ( int y = 0, i = 2; y < ICON_SIZE; y++ )
            {
            unsigned char *p = data + (y * stride);
            for ( int x = 0; x < ICON_SIZE*4; x += 4 )
                {
                unsigned char a, r, b, g;
//#ifdef __LITTLE_ENDIAN
#if __BYTE_ORDER == __BIG_ENDIAN
                a = p[x], r = p[x+1], g = p[x+2], b = p[x+3];
#else
                b = p[x], g = p[x+1], r = p[x+2], a = p[x+3];
#endif
                icondata[i] = a << 24 | r << 16 | g <<  8 | b;
                i++;
                }
            }   
        XChangeProperty(display, window,
                        XInternAtom(display, "_NET_WM_ICON", False),
                        XInternAtom(display, "CARDINAL", False),
                        32, PropModeReplace,
                        (unsigned char *)icondata, 2 + ICON_SIZE * ICON_SIZE);
        delete [] icondata;

        cairo_surface_destroy(surface);
        cairo_destroy(cr);
        }
    }

} // namespace uic
